iT邦幫忙

2024 iThome 鐵人賽

DAY 6
0

Day08 是要利用 HTML5 Canvas 來做出畫布

資料狀態

  • canvasRef, ctxRef 用來取得顯示的內容

  • isDrawing 判斷是否正在繪圖

  • hue 動態改變線條的顏色

  • direction 控制變化的方向

  • lastPositionRef 紀錄之前的繪圖定位

  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  const ctxRef = useRef<CanvasRenderingContext2D | null>(null);

  const [isDrawing, setIsDrawing] = useState<boolean>(false);
  const [hue, setHue] = useState<number>(0);
  const [direction, setDirection] = useState<boolean>(true);

  const lastPositionRef = useRef<Position>({ x: 0, y: 0 });

初始化

  const initializeCanvas = useCallback(() => {
    const canvas = canvasRef.current;
    if (!canvas) {
      return;
    }

    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;

    const ctx = canvas.getContext("2d");
    if (!ctx) {
      return;
    }

    ctx.strokeStyle = "#BADA55";
    ctx.lineJoin = "round";
    ctx.lineCap = "round";
    ctx.lineWidth = 1;
    ctxRef.current = ctx;
  }, []);

繪圖動作

  const draw = useCallback(
    (e: React.MouseEvent<HTMLCanvasElement>) => {
      if (!isDrawing) {
        return;
      }
      const ctx = ctxRef.current;
      if (!ctx) {
        return;
      }

      ctx.strokeStyle = `hsl(${hue}, 100%, 50%)`;
      ctx.beginPath();
      ctx.moveTo(lastPositionRef.current.x, lastPositionRef.current.y);
      ctx.lineTo(e.nativeEvent.offsetX, e.nativeEvent.offsetY);
      ctx.stroke();

      lastPositionRef.current = {
        x: e.nativeEvent.offsetX,
        y: e.nativeEvent.offsetY,
      };

      setHue((prevHue) => (prevHue + 1) % 360);

      if (ctx.lineWidth >= 100 || ctx.lineWidth <= 1) {
        setDirection((prevDirection) => !prevDirection);
      }

      ctx.lineWidth += direction ? 1 : -1;
    },
    [isDrawing, hue, direction]
  );

事件處理

  const startDrawing = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
    setIsDrawing(true);
    
    lastPositionRef.current = {
      x: e.nativeEvent.offsetX,
      y: e.nativeEvent.offsetY,
    };
  }, []);

  const stopDrawing = useCallback(() => {
    setIsDrawing(false);
  }, []);

resize

  • 讓畫布可以根據視窗動態調整
  const handleResize = useCallback(() => {
    initializeCanvas();
  }, [initializeCanvas]);

  useEffect(() => {
    initializeCanvas();

    window.addEventListener("resize", handleResize);
    return () => window.removeEventListener("resize", handleResize);
  }, [initializeCanvas, handleResize]);

結構

  return (
    <div className="w-screen h-screen bg-gray-100 p-4">
      <canvas
        ref={canvasRef}
        className="w-full h-full border rounded-md border-gray-300"
        onMouseDown={startDrawing}
        onMouseMove={draw}
        onMouseUp={stopDrawing}
        onMouseOut={stopDrawing}
      />
    </div>
  );

DEMO

https://codesandbox.io/p/devbox/3ngks9


上一篇
[Day07]_Array-Cardio-Day2
下一篇
[Day09]_Dev-Tools-Domination
系列文
React30——用 React 探索 JavaScript30 的魅力30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言